Skip to content

Emit gen_ai.client.token.usage and operation.duration metrics from the OTel middleware - #666

Open
PratikDhanave (PratikDhanave) wants to merge 5 commits into
microsoft:mainfrom
PratikDhanaveFork:otel-emit-token-usage-duration-metrics
Open

Emit gen_ai.client.token.usage and operation.duration metrics from the OTel middleware#666
PratikDhanave (PratikDhanave) wants to merge 5 commits into
microsoft:mainfrom
PratikDhanaveFork:otel-emit-token-usage-duration-metrics

Conversation

@PratikDhanave

Copy link
Copy Markdown
Contributor

What

The OTel middleware previously recorded token counts purely as span attributes (gen_ai.usage.*) via setUsage, and never measured run duration at all. It imported only otel/{attribute,codes,trace} and held a lone tracer — no Meter, no histograms.

Span attributes describe a single run and are not aggregatable across runs, so a spend/latency dashboard summing across many invocations saw nothing from the Go port. This change adds a Meter alongside the tracer and emits the two histograms the OTel GenAI semantic conventions define:

  • gen_ai.client.token.usage (unit {token}) — two data points per run, one tagged gen_ai.token.type=input (InputTokenCount) and one output (OutputTokenCount).
  • gen_ai.client.operation.duration (unit s) — one sample per run, measured from before span.Start.

Every data point carries the run's identifying attributes (gen_ai.operation.name, gen_ai.provider.name, gen_ai.agent.name), plus error.type when the run faulted. Recording is factored into a recordMetrics helper (mirroring setUsage) shared by both the normal post-loop path and the early-return path. Histogram construction errors are tolerated: NewMiddleware still returns a working tracer-only middleware and recordMetrics no-ops on a nil histogram.

Why (cross-SDK parity)

This aligns the Go port with the OTel GenAI semantic conventions and the Python/.NET references, which both emit these two histograms. Cross-language dashboards can now aggregate Go runs alongside the rest. Span-attribute accounting stays exactly as-is; metrics complement it (single-run detail vs. cross-run aggregation) rather than replacing it. Not a duplicate of the span PRs (#655/#646) or usage-accounting PRs (#594/#609/#551-554).

Tests

provider/otelprovider/otel_test.go gains two black-box tests driving an in-memory sdkmetric.ManualReader:

  • TestOtel_Run_RecordsUsageAndDurationMetrics — runs the middleware over a fake RunFunc yielding UsageContent (100 input / 50 output), collects ResourceMetrics, and asserts the two gen_ai.client.token.usage data points (input=100, output=50 by gen_ai.token.type) with the shared attributes, plus one gen_ai.client.operation.duration sample.
  • TestOtel_Run_MetricsIncludeErrorType — asserts error.type is present on the duration metric when the run faults.

Both fail before the change and pass after. go build ./..., go vet ./provider/otelprovider/..., and go test ./provider/otelprovider/... are green.

Open design questions

  • Scope: should these live in the existing tracing middleware (as here) or a separate metrics middleware/config toggle? Kept together since they share the accumulated usage and the run boundary.
  • API shape: MiddlewareConfig is unchanged — the Meter reuses SourceName for its instrumentation scope, matching the tracer. Open to a separate meter name if preferred.
  • Follow-ups: AdditionalCounts and cache/reasoning sub-counters are not yet emitted as metric data points (the conventions only define input/output token-type dimensions); could be added if there's appetite.

@PratikDhanave
PratikDhanave (PratikDhanave) force-pushed the otel-emit-token-usage-duration-metrics branch from 026998e to 5773147 Compare July 23, 2026 15:40
@github-actions

This comment has been minimized.

@PratikDhanave
PratikDhanave (PratikDhanave) force-pushed the otel-emit-token-usage-duration-metrics branch from 5773147 to 23dfb1a Compare July 24, 2026 01:40
@github-actions github-actions Bot added the parity-approved Go API consistency review found no parity issues label Jul 24, 2026
@github-actions

This comment has been minimized.

…el middleware

The OTel middleware recorded token counts only as span attributes, which are
not aggregatable across runs, so a spend/latency dashboard saw nothing from the
Go port. Add a Meter alongside the tracer and record the two histograms the
GenAI semantic conventions define and the Python/.NET SDKs emit:
gen_ai.client.token.usage ({token}) split by gen_ai.token.type input/output, and
gen_ai.client.operation.duration (s). Both are recorded on the normal and
early-return exit paths, tagged with operation/provider/agent name plus
error.type when the run faults. Histogram-construction failures leave a working
tracer-only middleware.
@PratikDhanave
PratikDhanave (PratikDhanave) force-pushed the otel-emit-token-usage-duration-metrics branch from 23dfb1a to 0076565 Compare July 24, 2026 09:34
@github-actions

This comment has been minimized.

# Conflicts:
#	go.mod
#	provider/otelprovider/otel.go
@github-actions

This comment has been minimized.

@PratikDhanave
PratikDhanave (PratikDhanave) marked this pull request as ready for review August 4, 2026 06:06
@PratikDhanave
PratikDhanave (PratikDhanave) requested a review from a team as a code owner August 4, 2026 06:06
Copilot AI lite review requested due to automatic review settings August 4, 2026 06:06

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR extends the provider/otelprovider OpenTelemetry middleware to emit GenAI semantic-convention metrics in addition to existing span attributes, enabling cross-run aggregation for token usage and operation duration (dashboards/alerts) while preserving per-run span detail.

Changes:

  • Add OTel Metrics instruments (gen_ai.client.token.usage, gen_ai.client.operation.duration) to the existing tracing middleware and record them per run.
  • Capture and attach shared identifying attributes (and error.type on failures) to emitted metric data points.
  • Add black-box tests using an in-memory sdkmetric.ManualReader to assert emitted histograms and error attribution.

Reviewed changes

Copilot reviewed 3 out of 4 changed files in this pull request and generated 1 comment.

File Description
provider/otelprovider/otel.go Adds Meter-based histograms and records token-usage + duration metrics from the middleware.
provider/otelprovider/otel_test.go Adds metric-focused tests validating histogram emission and error tagging.
go.mod Promotes OTel metric and sdk/metric dependencies to direct requirements.
go.sum Records new module checksums introduced by metric dependencies.

Comment on lines +166 to +173
attrs := []attribute.KeyValue{
attribute.String(attrKeyOperationName, opInvoke),
attribute.String(attrKeyProviderName, cmp.Or(a.ProviderName(), "unknown")),
attribute.String(attrKeyAgentName, a.Name()),
}
if errorType != "" {
attrs = append(attrs, attribute.String(attrKeyErrorType, errorType))
}
@gdams

Copy link
Copy Markdown
Member

PratikDhanave (@PratikDhanave) conflicts

@github-actions github-actions Bot added area:provider Changes files in the provider area area:provider/otel Changes files in the provider / otel area size:large At most 300 changed lines across at most 10 files pending-auto-risk Automatic risk classification is in progress labels Aug 20, 2026
@github-actions

This comment has been minimized.

@github-actions github-actions Bot added failed-auto-risk Automatic risk classification was inconclusive or failed and removed pending-auto-risk Automatic risk classification is in progress labels Aug 20, 2026
@github-actions github-actions Bot added pending-auto-risk Automatic risk classification is in progress risk:medium Contained production impact requiring normal review depth and removed failed-auto-risk Automatic risk classification was inconclusive or failed pending-auto-risk Automatic risk classification is in progress labels Aug 22, 2026
@github-actions

This comment has been minimized.

@github-actions github-actions Bot added pending-auto-risk Automatic risk classification is in progress risk:medium Contained production impact requiring normal review depth and removed risk:medium Contained production impact requiring normal review depth pending-auto-risk Automatic risk classification is in progress labels Aug 26, 2026
@github-actions

Copy link
Copy Markdown
Contributor

API Consistency Review

Scope: user-visible behavior (OTel metrics emission)

Changed Go contract: NewMiddleware now creates two metric.Float64Histogram instruments — gen_ai.client.token.usage ({token}) and gen_ai.client.operation.duration (s) — and emits them from recordMetrics on every run. The exported MiddlewareConfig type and NewMiddleware signature are unchanged; the added histogram fields are on the unexported mw struct.

Upstream evidence reviewed:

  • python/packages/core/agent_framework/observability.py lines 157–188 (TOKEN_USAGE_BUCKET_BOUNDARIES, OPERATION_DURATION_BUCKET_BOUNDARIES), lines 1857–1871 (_get_duration_histogram, _get_token_usage_histogram), lines 3523–3537 (metric emission)
  • .NET: dotnet/src/Microsoft.Agents.AI/OpenTelemetryAgent.cs — delegates histogram emission to OpenTelemetryChatClient from Microsoft.Extensions.AI, which itself uses the .NET OTel SDK views/advisory boundaries

Result: one parity finding reported — explicit histogram bucket boundaries


Finding: Missing explicit histogram bucket boundaries

The Python implementation explicitly sets explicit_bucket_boundaries_advisory on both histograms, matching the advisory boundaries specified by the OpenTelemetry GenAI semantic conventions:

# python/packages/core/agent_framework/observability.py:157–188
TOKEN_USAGE_BUCKET_BOUNDARIES: Final[tuple[float, ...]] = (
    1, 4, 16, 64, 256, 1024, 4096, 16384, 65536, 262144, 1048576, 4194304, 16777216, 67108864,
)
OPERATION_DURATION_BUCKET_BOUNDARIES: Final[tuple[float, ...]] = (
    0.01, 0.02, 0.04, 0.08, 0.16, 0.32, 0.64, 1.28, 2.56, 5.12, 10.24, 20.48, 40.96, 81.92,
)

# passed as:
meter.create_histogram(..., explicit_bucket_boundaries_advisory=TOKEN_USAGE_BUCKET_BOUNDARIES)

The Go PR creates the histograms without equivalent bucket advisory options:

// provider/otelprovider/otel.go — NewMiddleware
if h, err := meter.Float64Histogram(
    metricTokenUsage,
    metric.WithUnit("{token}"),
    metric.WithDescription("Measures number of input and output tokens used."),
); err == nil {
    m.tokenUsage = h
}

Semantic impact: Omitting the advisory boundaries means the Go OTel SDK uses its own default exponential/explicit-boundary sequence, which differs from the Python advisory sequence. Cross-language dashboards that aggregate histogram data from Go and Python agents will see different bucket distributions for identical workloads, making percentile (p50/p95) and heatmap comparisons misleading across SDKs. The total count and sum remain comparable, but the key point of histogram alignment is bucket-level comparability.

Suggested resolution: Pass metric.WithExplicitBucketBoundaries(...) (from go.opentelemetry.io/otel/sdk/metric) or the equivalent OTel Go advisory option when building both histograms in NewMiddleware, using the same boundary values as the Python SDK:

// Token usage buckets (powers-of-4, matching OTel GenAI advisory)
var tokenUsageBuckets = []float64{1, 4, 16, 64, 256, 1024, 4096, 16384, 65536, 262144, 1048576, 4194304, 16777216, 67108864}

// Operation duration buckets (doubling sequence in seconds, matching OTel GenAI advisory)
var operationDurationBuckets = []float64{0.01, 0.02, 0.04, 0.08, 0.16, 0.32, 0.64, 1.28, 2.56, 5.12, 10.24, 20.48, 40.96, 81.92}

Note: In the OTel Go SDK the advisory boundaries are typically applied at the MeterProvider level via a View, not in the Histogram constructor call, which keeps this change in NewMiddleware but may require a helper or documented setup instruction for users who bring their own MeterProvider.


Because a parity issue was found, the parity-approved label has been removed.

Generated by Go API Consistency Review Agent · sonnet46 · 47.5 AIC · ⌖ 4.43 AIC · ⊞ 6.4K ·

@github-actions github-actions Bot removed the parity-approved Go API consistency review found no parity issues label Aug 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:provider/otel Changes files in the provider / otel area area:provider Changes files in the provider area risk:medium Contained production impact requiring normal review depth size:large At most 300 changed lines across at most 10 files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants